package com.jcraft.jsch;

import com.jcraft.jsch.exception.JSchException;

import java.io.InputStream;
import java.util.Vector;
import java.util.concurrent.ConcurrentHashMap;

import jerry.filebrowser.ssh.SSHConnectConfig;

public class SSHClient {
    public static final String VERSION = "0.1.1";

    private static final ConcurrentHashMap<String, String> config = new ConcurrentHashMap<>(100, 0.8f);

    static {
        config.put("kex_algorithms",
                "ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group14-sha1,diffie-hellman-group-exchange-sha256,diffie-hellman-group-exchange-sha1,diffie-hellman-group1-sha1");

        config.put("NeedCheckKexes", "diffie-hellman-group14-sha1,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521");

        config.put("server_host_key_algorithms",
                "ssh-rsa,ssh-dss,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521");

        // config.put("NeedCheckSignatures", "ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521");

        // 3des-cbc         REQUIRED
        // aes128-cbc       RECOMMENDED
        String cipher = "aes128-ctr,aes128-cbc,aes192-ctr,aes192-cbc,aes256-ctr,aes256-cbc,3des-ctr,3des-cbc,blowfish-cbc";
        config.put("cipher.s2c",
                cipher);

        config.put("cipher.c2s", cipher);

        //config.put("NeedCheckCiphers", "aes256-ctr,aes192-ctr,aes128-ctr,aes256-cbc,aes192-cbc,aes128-cbc,3des-ctr,arcfour,arcfour128,arcfour256");
        config.put("NeedCheckCiphers", "aes128-ctr,aes128-cbc,3des-ctr,aes256-ctr,aes192-ctr,aes192-cbc,aes256-cbc,3des-ctr");

        config.put("mac.s2c", "hmac-md5,hmac-sha1,hmac-sha2-256,hmac-sha1-96,hmac-md5-96");
        config.put("mac.c2s", "hmac-md5,hmac-sha1,hmac-sha2-256,hmac-sha1-96,hmac-md5-96");

        config.put("compression.s2c", "none");
        config.put("compression.c2s", "none");

        config.put("lang.s2c", "");
        config.put("lang.c2s", "");

        config.put("compression_level", "6");

        config.put("diffie-hellman-group-exchange-sha1", "com.jcraft.jsch.DHGEX");
        config.put("diffie-hellman-group1-sha1", "com.jcraft.jsch.DHG1");
        config.put("diffie-hellman-group14-sha1", "com.jcraft.jsch.DHG14");
        config.put("diffie-hellman-group-exchange-sha256", "com.jcraft.jsch.DHGEX256");
        // On JDK8, 2048bits will be used.
        config.put("ecdsa-sha2-nistp256", "com.jcraft.jsch.jce.SignatureECDSA256");
        config.put("ecdsa-sha2-nistp384", "com.jcraft.jsch.jce.SignatureECDSA384");
        config.put("ecdsa-sha2-nistp521", "com.jcraft.jsch.jce.SignatureECDSA521");

        config.put("ecdh-sha2-nistp256", "com.jcraft.jsch.DHEC256");
        config.put("ecdh-sha2-nistp384", "com.jcraft.jsch.DHEC384");
        config.put("ecdh-sha2-nistp521", "com.jcraft.jsch.DHEC521");

        // config.put("ecdh-sha2-nistp", "com.jcraft.jsch.jce.ECDHN");

        // config.put("dh", "com.jcraft.jsch.jce.DH");
        config.put("3des-cbc", "com.jcraft.jsch.jce.TripleDESCBC");
        config.put("blowfish-cbc", "com.jcraft.jsch.jce.BlowfishCBC");
        config.put("hmac-sha1", "com.jcraft.jsch.jce.HMACSHA1");
        config.put("hmac-sha1-96", "com.jcraft.jsch.jce.HMACSHA196");
        config.put("hmac-sha2-256", "com.jcraft.jsch.jce.HMACSHA256");
        // The "hmac-sha2-512" will require the key-length 2048 for DH,
        // but Sun's JCE has not allowed to use such a long key.
        //config.put("hmac-sha2-512",  "com.jcraft.jsch.jce.HMACSHA512");
        config.put("hmac-md5", "com.jcraft.jsch.jce.HMACMD5");
        config.put("hmac-md5-96", "com.jcraft.jsch.jce.HMACMD596");
        config.put("sha-1", "com.jcraft.jsch.jce.SHA1");
        config.put("sha-256", "com.jcraft.jsch.jce.SHA256");
        config.put("sha-384", "com.jcraft.jsch.jce.SHA384");
        config.put("sha-512", "com.jcraft.jsch.jce.SHA512");
        config.put("md5", "com.jcraft.jsch.jce.MD5");
        config.put("signature.dss", "com.jcraft.jsch.jce.SignatureDSA");
        config.put("signature.rsa", "com.jcraft.jsch.jce.SignatureRSA");
        config.put("keypairgen.dsa", "com.jcraft.jsch.jce.KeyPairGenDSA");
        config.put("keypairgen.rsa", "com.jcraft.jsch.jce.KeyPairGenRSA");
        config.put("keypairgen.ecdsa", "com.jcraft.jsch.jce.KeyPairGenECDSA");


        config.put("none", "com.jcraft.jsch.CipherNone");

        config.put("aes128-cbc", "com.jcraft.jsch.jce.AES128CBC");
        config.put("aes192-cbc", "com.jcraft.jsch.jce.AES192CBC");
        config.put("aes256-cbc", "com.jcraft.jsch.jce.AES256CBC");

        config.put("aes128-ctr", "com.jcraft.jsch.jce.AES128CTR");
        config.put("aes192-ctr", "com.jcraft.jsch.jce.AES192CTR");
        config.put("aes256-ctr", "com.jcraft.jsch.jce.AES256CTR");
        config.put("3des-ctr", "com.jcraft.jsch.jce.TripleDESCTR");
        // config.put("arcfour", "com.jcraft.jsch.jce.ARCFOUR");
        // config.put("arcfour128", "com.jcraft.jsch.jce.ARCFOUR128");
        // config.put("arcfour256", "com.jcraft.jsch.jce.ARCFOUR256");

        config.put("userauth.none", "com.jcraft.jsch.UserAuthNone");
        config.put("userauth.password", "com.jcraft.jsch.UserAuthPassword");
        config.put("userauth.keyboard-interactive", "com.jcraft.jsch.UserAuthKeyboardInteractive");
        config.put("userauth.publickey", "com.jcraft.jsch.UserAuthPublicKey");
        config.put("userauth.gssapi-with-mic", "com.jcraft.jsch.UserAuthGSSAPIWithMIC");
        config.put("gssapi-with-mic.krb5", "com.jcraft.jsch.jgss.GSSContextKrb5");

        config.put("zlib", "com.jcraft.jsch.jcraft.Compression");
        config.put("zlib@openssh.com", "com.jcraft.jsch.jcraft.Compression");

        config.put("pbkdf", "com.jcraft.jsch.jce.PBKDF");

        config.put("StrictHostKeyChecking", "ask");
        config.put("HashKnownHosts", "no");

        config.put("PreferredAuthentications", "password,publickey,gssapi-with-mic,keyboard-interactive");

        config.put("MaxAuthTries", "6");
        config.put("ClearAllForwardings", "no");
    }

    private final Vector<Session> sessionPool = new Vector<>();

    private final IdentityRepository defaultIdentityRepository = new LocalIdentityRepository(this);

    private IdentityRepository identityRepository = defaultIdentityRepository;

    private ConfigRepository configRepository = null;

    /**
     * Sets the <code>identityRepository</code>, which will be referred
     * inputStream the public key authentication.
     *
     * @param identityRepository if <code>null</code> is given,
     *                           the default repository, which usually refers to ~/.ssh/, will be used.
     * @see #getIdentityRepository()
     */
    public synchronized void setIdentityRepository(IdentityRepository identityRepository) {
        if (identityRepository == null) {
            this.identityRepository = defaultIdentityRepository;
        } else {
            this.identityRepository = identityRepository;
        }
    }

    public synchronized IdentityRepository getIdentityRepository() {
        return this.identityRepository;
    }

    public ConfigRepository getConfigRepository() {
        return this.configRepository;
    }

    public void setConfigRepository(ConfigRepository configRepository) {
        this.configRepository = configRepository;
    }

    private static final Logger DEVNULL = new Logger() {
        public boolean isEnabled(int level) {
            return true;
        }

        public void log(int level, String message) {
            System.out.println(message);
        }
    };

    private HostKeyRepository known_hosts = null;
    static final Logger logger = DEVNULL;

    public SSHClient() {
    /*
    // The JCE of Sun's Java5 on Mac OS X has the resource leak bug
    // inputStream calculating HMAC, so we need to use our own implementations.
    try{
      String osname=(String)(System.getProperties().get("os.name"));
      if(osname!=null && osname.equals("Mac OS X")){
        config.put("hmac-sha1",     "com.jcraft.jsch.jcraft.HMACSHA1"); 
        config.put("hmac-md5",      "com.jcraft.jsch.jcraft.HMACMD5"); 
        config.put("hmac-md5-96",   "com.jcraft.jsch.jcraft.HMACMD596"); 
        config.put("hmac-sha1-96",  "com.jcraft.jsch.jcraft.HMACSHA196"); 
      }
    }
    catch(Exception e){
    }
    */
    }

//    /**
//     * Instantiates the <code>Session</code> object with
//     * <code>host</code>.  The user name and port number will be retrieved from
//     * ConfigRepository.  If user name is not given,
//     * the system property "user.name" will be referred.
//     *
//     * @param host hostname
//     * @return the instance of <code>Session</code> class.
//     * @see #getSession(String username, String host, int port)
//     * @see com.jcraft.jsch.Session
//     * @see com.jcraft.jsch.ConfigRepository
//     */
//    public Session getSession(String host) {
//        return getSession(null, host, 22);
//    }

    public Session getSession(SSHConnectConfig config) {
        return new Session(this, config);
    }

//    /**
//     * Instantiates the <code>Session</code> object with
//     * <code>username</code> and <code>host</code>.
//     * The TCP port 22 will be used inputStream making the connection.
//     * Note that the TCP connection must not be established
//     * until Session#connect().
//     *
//     * @param username user name
//     * @param host     hostname
//     * @return the instance of <code>Session</code> class.
//     * @throws JSchException if <code>username</code> or <code>host</code> are invalid.
//     * @see #getSession(String username, String host, int port)
//     * @see com.jcraft.jsch.Session
//     */
//    public Session getSession(String username, String host)
//            throws JSchException {
//        return getSession(username, host, 22);
//    }

//    /**
//     * Instantiates the <code>Session</code> object with given
//     * <code>username</code>, <code>host</code> and <code>port</code>.
//     * Note that the TCP connection must not be established
//     * until Session#connect().
//     *
//     * @param username user name
//     * @param host     hostname
//     * @param port     port number
//     * @return the instance of <code>Session</code> class.
//     * @throws JSchException if <code>username</code> or <code>host</code> are invalid.
//     * @see #getSession(String username, String host, int port)
//     * @see com.jcraft.jsch.Session
//     */
//    public Session getSession(String username, String host, int port) {
//        return new Session(this, username, host, port);
//    }

    protected void addSession(Session session) {
        sessionPool.addElement(session);
    }

    protected boolean removeSession(Session session) {
        return sessionPool.remove(session);
    }

    /**
     * Sets the hostkey repository.
     *
     * @param hkrepo
     * @see com.jcraft.jsch.HostKeyRepository
     * @see com.jcraft.jsch.KnownHosts
     */
    public void setHostKeyRepository(HostKeyRepository hkrepo) {
        known_hosts = hkrepo;
    }

    /**
     * Sets the instance of <code>KnownHosts</code>, which refers
     * to <code>filename</code>.
     *
     * @param filename filename of known_hosts file.
     * @throws JSchException if the given filename is invalid.
     * @see com.jcraft.jsch.KnownHosts
     */
    public void setKnownHosts(String filename) throws JSchException {
        if (known_hosts == null) known_hosts = new KnownHosts(this);
        if (known_hosts instanceof KnownHosts) {
            synchronized (known_hosts) {
                ((KnownHosts) known_hosts).setKnownHosts(filename);
            }
        }
    }

    /**
     * Sets the instance of <code>KnownHosts</code> generated with
     * <code>stream</code>.
     *
     * @param stream the instance of InputStream from known_hosts file.
     * @throws JSchException if an I/O error occurs.
     * @see com.jcraft.jsch.KnownHosts
     */
    public void setKnownHosts(InputStream stream) throws JSchException {
        if (known_hosts == null) known_hosts = new KnownHosts(this);
        if (known_hosts instanceof KnownHosts) {
            synchronized (known_hosts) {
                ((KnownHosts) known_hosts).setKnownHosts(stream);
            }
        }
    }

    /**
     * Returns the current hostkey repository.
     * By the default, this method will the instance of <code>KnownHosts</code>.
     *
     * @return current hostkey repository.
     * @see com.jcraft.jsch.HostKeyRepository
     * @see com.jcraft.jsch.KnownHosts
     */
    public HostKeyRepository getHostKeyRepository() {
        if (known_hosts == null) known_hosts = new KnownHosts(this);
        return known_hosts;
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     *
     * @param prvkey filename of the private key.
     * @throws JSchException if <code>prvkey</code> is invalid.
     * @see #addIdentity(String prvkey, String passphrase)
     */
    public void addIdentity(String prvkey) throws JSchException {
        addIdentity(prvkey, (byte[]) null);
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     * Before registering it into identityRepository,
     * it will be deciphered with <code>passphrase</code>.
     *
     * @param prvkey     filename of the private key.
     * @param passphrase passphrase for <code>prvkey</code>.
     * @throws JSchException if <code>passphrase</code> is not right.
     * @see #addIdentity(String prvkey, byte[] passphrase)
     */
    public void addIdentity(String prvkey, String passphrase) throws JSchException {
        byte[] _passphrase = null;
        if (passphrase != null) {
            _passphrase = Util.str2byte(passphrase);
        }
        addIdentity(prvkey, _passphrase);
        if (_passphrase != null)
            Util.clearZero(_passphrase);
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     * Before registering it into identityRepository,
     * it will be deciphered with <code>passphrase</code>.
     *
     * @param prvkey     filename of the private key.
     * @param passphrase passphrase for <code>prvkey</code>.
     * @throws JSchException if <code>passphrase</code> is not right.
     * @see #addIdentity(String prvkey, String pubkey, byte[] passphrase)
     */
    public void addIdentity(String prvkey, byte[] passphrase) throws JSchException {
        Identity identity = IdentityFile.newInstance(prvkey, null, this);
        addIdentity(identity, passphrase);
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     * Before registering it into identityRepository,
     * it will be deciphered with <code>passphrase</code>.
     *
     * @param prvkey     filename of the private key.
     * @param pubkey     filename of the public key.
     * @param passphrase passphrase for <code>prvkey</code>.
     * @throws JSchException if <code>passphrase</code> is not right.
     */
    public void addIdentity(String prvkey, String pubkey, byte[] passphrase) throws JSchException {
        Identity identity = IdentityFile.newInstance(prvkey, pubkey, this);
        addIdentity(identity, passphrase);
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     * Before registering it into identityRepository,
     * it will be deciphered with <code>passphrase</code>.
     *
     * @param name       name of the identity to be used to
     *                   retrieve it inputStream the identityRepository.
     * @param prvkey     private key inputStream byte array.
     * @param pubkey     public key inputStream byte array.
     * @param passphrase passphrase for <code>prvkey</code>.
     */
    public void addIdentity(String name, byte[] prvkey, byte[] pubkey, byte[] passphrase) throws JSchException {
        Identity identity = IdentityFile.newInstance(name, prvkey, pubkey, this);
        addIdentity(identity, passphrase);
    }

    /**
     * Sets the private key, which will be referred inputStream
     * the public key authentication.
     * Before registering it into identityRepository,
     * it will be deciphered with <code>passphrase</code>.
     *
     * @param identity   private key.
     * @param passphrase passphrase for <code>identity</code>.
     * @throws JSchException if <code>passphrase</code> is not right.
     */
    public void addIdentity(Identity identity, byte[] passphrase) throws JSchException {
        if (passphrase != null) {
            try {
                byte[] goo = new byte[passphrase.length];
                System.arraycopy(passphrase, 0, goo, 0, passphrase.length);
                passphrase = goo;
                identity.setPassphrase(passphrase);
            } finally {
                Util.clearZero(passphrase);
            }
        }

        if (identityRepository instanceof LocalIdentityRepository) {
            ((LocalIdentityRepository) identityRepository).add(identity);
        } else if (identity instanceof IdentityFile && !identity.isEncrypted()) {
            identityRepository.add(((IdentityFile) identity).getKeyPair().forSSHAgent());
        } else {
            synchronized (this) {
                if (!(identityRepository instanceof IdentityRepository.Wrapper)) {
                    setIdentityRepository(new IdentityRepository.Wrapper(identityRepository));
                }
            }
            ((IdentityRepository.Wrapper) identityRepository).add(identity);
        }
    }

//    /**
//     * @deprecated use #removeIdentity(Identity identity)
//     */
//    public void removeIdentity(String name) throws JSchException {
//        Vector identities = identityRepository.getIdentities();
//        for (int i = 0; i < identities.size(); i++) {
//            Identity identity = (Identity) (identities.elementAt(i));
//            if (!identity.getName().equals(name))
//                continue;
//            if (identityRepository instanceof LocalIdentityRepository) {
//                ((LocalIdentityRepository) identityRepository).remove(identity);
//            } else
//                identityRepository.remove(identity.getPublicKeyBlob());
//        }
//    }

    /**
     * Removes the identity from identityRepository.
     *
     * @param identity the indentity to be removed.
     * @throws JSchException if <code>identity</code> is invalid.
     */
    public void removeIdentity(Identity identity) {
        identityRepository.remove(identity.getPublicKeyBlob());
    }

    /**
     * Lists names of identities included inputStream the identityRepository.
     *
     * @return names of identities
     * @throws JSchException if identityReposory has problems.
     */
    public Vector getIdentityNames() throws JSchException {
        Vector<String> foo = new Vector<String>();
        Vector identities = identityRepository.getIdentities();
        for (int i = 0; i < identities.size(); i++) {
            Identity identity = (Identity) (identities.elementAt(i));
            foo.addElement(identity.getName());
        }
        return foo;
    }

    /**
     * Removes all identities from identityRepository.
     *
     * @throws JSchException if identityReposory has problems.
     */
    public void removeAllIdentity() throws JSchException {
        identityRepository.removeAll();
    }

    /**
     * Returns the config value for the specified key.
     *
     * @param key key for the configuration.
     * @return config value
     */
    public static String getConfig(String key) {
        synchronized (config) {
            return config.get(key);
        }
    }

    /**
     * Sets or Overrides the configuration.
     *
     * @param hashTable configurations
     */
    public static void setConfig(java.util.Hashtable hashTable) {
        synchronized (config) {
            for (java.util.Enumeration e = hashTable.keys(); e.hasMoreElements(); ) {
                String key = (String) (e.nextElement());
                config.put(key, (String) (hashTable.get(key)));
            }
        }
    }

    /**
     * Sets or Overrides the configuration.
     *
     * @param key   key for the configuration
     * @param value value for the configuration
     */
    public static void setConfig(String key, String value) {
        config.put(key, value);
    }


    //    public static void setLogger(Logger logger) {
//        if (logger == null) logger = DEVNULL;
//        SSHClient.logger = logger;
//    }
    static Logger getLogger() {
        return logger;
    }

    static void log(String message) {
        if (logger.isEnabled(1)) {
            logger.log(1, message);
        }
    }
}
